#!/bin/sh
usage () { echo Usage: `basename $0` "[-h|-u] [-v] [--] prior soon"; }
#############################################################################
##
## Copyright (C) 2016 The Qt Company Ltd.
## Contact: https://www.qt.io/licensing/
##
## This file is part of the release tools of the Qt Toolkit.
##
## $QT_BEGIN_LICENSE:LGPL$
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and The Qt Company. For licensing terms
## and conditions see https://www.qt.io/terms-conditions. For further
## information use the contact form at https://www.qt.io/contact-us.
##
## GNU Lesser General Public License Usage
## Alternatively, this file may be used under the terms of the GNU Lesser
## General Public License version 3 as published by the Free Software
## Foundation and appearing in the file LICENSE.LGPL3 included in the
## packaging of this file. Please review the following information to
## ensure the GNU Lesser General Public License version 3 requirements
## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
##
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 2.0 or (at your option) the GNU General
## Public license version 3 or any later version approved by the KDE Free
## Qt Foundation. The licenses are as published by the Free Software
## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
## included in the packaging of this file. Please review the following
## information to ensure the GNU General Public License requirements will
## be met: https://www.gnu.org/licenses/gpl-2.0.html and
## https://www.gnu.org/licenses/gpl-3.0.html.
##
## $QT_END_LICENSE$
##
#############################################################################

help () {
    usage
    cat <<EOF

Prepare a commit for pushing to Gerrit, that expresses the interesting
changes in API between a prior release (tag or branch) and an imminent
release (branch).  Certain boring changes are excluded; you can review
them after this has run with git diff -D; if necessary, you can amend
the commit to remove any remaining boredom or include anything
mis-classified as boring, before pushing to Gerrit.

Run in the top-level directory of a work tree in a clean state; don't
expect it to be left in a clean state. In particular, any sub-modules
shall be checked out on their versions from the prior release.

Depends on the dulwich package for python; see your local package
manager or 'pip install dulwich' if managing python packages with pip.

Options:

 -u
 --usage   Print simple usage summary (first line above) and exit.

 -h
 --help    Print this help and exit.

 -t ID
 --task ID
 --task-number ID
           Include a Task-number: in the commit message, with the
           given id, ID, of the bug-tracker issue being used to keep
           track of the API change review.

 -a
 --amend   Amend existing commit rather than extending the review
           branch, if it exists already.

 -r
 --replace Replace existing branch, if present, re-using its last
           Change-Id, rather than updating it.  (Conflicts with
           --amend, which should be preferred unless these scripts
           have changed.  If both are used, the last wins.)  If
           --task-number has not been specified, any Task-number:
           footer in the earlier commit will also be preserved.

 -v
 --verbose Say what's happening as it happens.

 -q
 --quiet   Don't mention anything but errors.

 --        End option processing.

Arguments:

 prior     The commit (branch, tag or sha1) of a prior release.
 soon      The branch approaching release.

You should see the usual git output describing the commit created.
You should verify that git diff -D is all boring and the commit is all
interesting before pushing this commit to Gerrit.

Exit status is 1 on error, else 0.  Success may mean no change at all,
only boring changes or some interesting changes have been saved.  If
there are no changes, the repository is restored to its prior state
(including deleting the api-review-* branch, if created rather than
reused), except for any sub-modules.  The other two cases can be
distinguished by comparing the api-review-* branch with the prior
release using git diff --quiet; this succeeds if there is no
difference - i.e. if the API change was all boring so we made no
commit.

After a first pass of review, if prior or soon has changed (and been
fetched), it is possible to rebase the review branch onto prior and
re-run this script to update the review.  If you pass the --amend
option and there was a prior commit on the branch, that commit shall
be amended rather than a new commit added to the branch.  Otherwise,
any change shall be recorded as a fresh commit - which you can always
squash onto an earlier commit later, after reviewing it, if you prefer
that work-flow so left out --amend.  Either way, you can then push the
squashed or amended commit to Gerrit for re-review, as usual.
EOF
}
warn () { echo "$@" >&2; }
die () { warn "$@"; exit 1; }
run () { mutter "Running: $@"; eval "$@" || die "Failed ($?): $@"; }

# Check basic expectations of context:
if [ -d src -a -f sync.profile ]
then
    SYNC='sync.profile'
    SRCDIR=src
elif [ -f Source/sync.profile ] # qtwebkit/'s eccentric layout ...
then
    SYNC='Source/sync.profile'
    SRCDIR=Source
else
    die "I expect to be run in the top level directory of a module (see --help)."
fi
THERE=`dirname $0`
[ -n "$THERE" -a -x "$THERE/resetboring.py" ] || \
    die "I don't know where resetboring.py is: please run me via an explicit path."
python -c 'from dulwich.repo import Repo; from dulwich.index import IndexEntry' || \
    die "I need dulwich installed (for resetboring.py; see --help)."
# dulwich 0.16.3 has been known to work; 0.9.4 is too old.

# Parse command-line:
CHATTY=
AMEND=
bad () { usage >&2; die "$@"; }
while [ $# -gt 0 ]
do  case $1 in
        -u|--usage) usage; exit 0 ;;
        -h|--help) help; exit 0 ;;
        -a|--amend) AMEND=--amend; shift ;;
        -r|--replace) AMEND=--replace; shift ;;
        -t|--task|--task-number) TASK="$2"; shift 2 ;;
        -v|--verbose) CHATTY=more; shift ;;
        -q|--quiet) CHATTY=less; shift ;;
        --) shift; break ;;
        -*) bad "Unrecognised option: $1" ;;
        *) break ;;
    esac
done

# Select revisions to compare:
[ $# -eq 2 ] || bad "Expected exactly two arguments, got $#: $@"
for arg
do git rev-parse "$arg" -- >/dev/null || bad "Failed to parse $arg as a git ref"
done
PRIOR="$1"
RELEASE="$2"
RESTORE="`git branch | sed -n -e '/^\* (HEAD/ s/.* \([^ ]*\))$/\1/ p' -e '/^\*/ s/.* // p'`"

# Implement --verbose, --quiet:
QUIET=
mutter () { true; }
mention () { warn "$@"; }
case "$CHATTY" in
    more) mutter () { warn "$@"; } ;;
    less) QUIET=-q
        mention () { true; }
        ;;
    *) ;;
esac
retire () { mention "$@"; exit; }
checkout () { run git checkout $QUIET "$@"; }

# Get API headers of $RELEASE checked out on a branch off $PRIOR:
BRANCH="api-review-$PRIOR-$RELEASE"
mutter "Checking for branch $BRANCH to check out"
case `git branch | grep -wF " $BRANCH" | grep "^[* ] $BRANCH"'$'` in
    '')
        checkout -b "$BRANCH" "$PRIOR"
        NEWBRANCH=yes
        if [ -n "$AMEND" ]
        then
            mention "Ignoring requested $AMEND: no prior $BRANCH"
            AMEND=
        fi
        ;;
    '* '*)
        case "$AMEND" in
            '--replace')
                mutter "On prior branch $BRANCH; shall be removed and recreated"
                ;;
            '--amend')
                mutter "Already on branch $BRANCH; preparing to amend it"
                ;;
            *)
                mutter "Already on branch $BRANCH; preparing to extend it"
                ;;
        esac
        ;;
    '  '*)
        case "$AMEND" in
            '--replace')
                mutter "Replacing existing branch $BRANCH (reusing its Change-Id)"
                ;;
            '--amend')
                mutter "Reusing existing branch $BRANCH; preparing to amend it"
                checkout "$BRANCH"
                ;;
            *)
                mutter "Reusing existing branch $BRANCH; preparing to extend it"
                checkout "$BRANCH"
                ;;
        esac
        ;;
esac

# Implement --replace and --amend:
CHANGEID=
if [ -n "$AMEND" ]
then
    # Suppress --amend or --replace unless we have a prior commit on $BRANCH:
    if git diff --quiet "$BRANCH" "$PRIOR"
    then
        mention "Suppressing requested $AMEND: no prior commit on $BRANCH"
        AMEND=
    else # Read (and validate) last commit's Change-Id:
        CHANGEID=`git show --summary $BRANCH | sed -ne '/Change-Id:/ s/.*: *//p'`
        [ -n "$CHANGEID" ] || die "No prior Change-Id from $BRANCH"
        expr "$CHANGEID" : "^I[0-9a-f]\{40\}$" >/dev/null || \
            die "Bad prior Change-Id ($CHANGEID) from $BRANCH"
        # Also preserve Task-number, if present and not over-ridden:
        [ -n "$TASK" ] || TASK=`git show --summary $BRANCH | sed -ne '/Task-number:/ s/.*: *//p'`
    fi
fi
if [ "$AMEND" = '--replace' ]
then
    AMEND=
    checkout "$RELEASE"
    run git branch -D "$BRANCH"
    checkout -b "$BRANCH" "$PRIOR"
fi
# Even when we do have a prior commit, the headers it reports as
# deleted are not actually deleted as part of that commit; so their
# deletion below shall ensure they're reported in the commit message,
# whether AMENDing or not.  We could filter these when not AMENDing,
# but (doing so would be fiddly and) any restored would then be
# described as deleted in the first commit's message, without
# mentioning that they're restored in the second (albeit any change in
# them shall show up in the diff).

# apiheaders commit
# Echoes one header name per line
apiheaders () {
    # Preliminary file-list, to be filtered:
    git ls-tree -r --name-only "$1" \
        | grep "^$SRCDIR"'/[-a-zA-Z0-9_/]*\.h$' \
        | grep -vi '^src/tools/' \
        | grep -v _p/ \
        | grep -v '_\(p\|pch\)\.h$' \
        | grep -v '/qt[a-z0-9][a-z0-9]*-config\.h$' \
        | grep -v '/\.' \
        | grep -vi '/\(private\|doc\|tests\|examples\|build\)/' \
        | grep -v '/ui_[^/]*\.h$' \
  | if git checkout "$1" -- $SYNC >&2
    then
        mutter "Using $SYNC's list of API headers"
        "$THERE"/sync-filter-api-headers \
            || warn "Failed ($?) to filter header list for $1"
    else
        mutter "No $SYNC in $1: falling back on filtered git ls-tree"
        while read f # Add some further kludgy filtering:
        do case "$f" in
                # qtbase just has to be different:
                src/plugins/platforms/eglfs/api/*) echo "$f" ;;
                src/3rdparty/angle/include/*) echo "$f" ;;
                # Otherwise, plugins and 3rdparty aren't API:
                src/plugins/*) ;;
                */3rdparty/*) ;;
                *) echo "$f" ;;
           esac
        done
    fi
}

# Make sure any sub-submodules are in their right states:
git submodule update --checkout

mutter "Purging obsolete headers" # so renames get detected and handled correctly:
apiheaders HEAD | while read h
# Update former API headers, remove them if removed:
do git checkout -q "$RELEASE" -- "$h" || git rm $QUIET -f -- "$h"
done 2>&1 | grep -wv "error: pathspec '.*' did not match any"
mutter "Checking out $RELEASE's API headers"
apiheaders "$RELEASE" | tr '\n' '\0' | \
    xargs -0r git checkout "$RELEASE" -- || die "Failed new header checkout"

git diff --quiet || die "All changes should initially be staged."
if git diff --quiet --cached "$PRIOR"
then
    mutter "Clearing away unused branch and restoring $RESTORE"
    git checkout $QUIET "$RESTORE"
    git branch -D "$BRANCH"
    retire "No changes to API (not even boring ones)"
fi

mutter "Reverting the boring bits"
run "$THERE/resetboring.py" --disclaim | xargs -r git checkout $QUIET "$PRIOR" --

if git diff --quiet --cached "$PRIOR"
then retire "All the change here looks boring: check with git diff -D"
fi

# Find a good place to prepare our commit message
if [ -f .git ]
then GITDIR=`cut -d ' ' -f 2 <.git`
else GITDIR=.git
fi

# It's a WIP to discourage any unwitting actual merge !
( echo "WIP: API comparison from $PRIOR to $RELEASE"
  echo
  git status | grep 'deleted:' | tr '\t' ' '
  git diff | wc | python -c 'import sys; row = sys.stdin.readline().split(); print; \
        print (("Excluded %s lines (%s words, %s bytes) of boring changes." % tuple(row)) \
              if any(int(x) != 0 for x in row) else "Found nothing boring to ignore.")'
  cat <<EOF

Note to reviewers: if you know of other changes to API, not covered by
this review, fetch this from Gerrit - see its checkout instructions;
you might want to add -b $BRANCH to those - and you
can add files to the result.  This can be relevant for behavior
changes in the .cpp or, for changes to documented behavior, in .qdoc
files.

Just git checkout $RELEASE each relevant file; you can then git reset
HEAD each and git add -p to select the relevant changes or use git
reset -p HEAD to unstage what git checkout has staged.  Try not to
include extraneous (non-API) changes.  Once you've added what matters,
git commit --amend and push back to gerrit's refs/for/$RELEASE in the
usual way.

Between staging changes from $RELEASE and committing, you can filter
some boring changes by running qtqa/scripts/api-review/resetboring.py
in the top-level directory of your module; this selectively de-stages
things commonly deemed boring in review.  (You'll need the python
dulwich package installed, for this.)  You can git add -p after that,
of course, to restore any changes you think aren't boring.

Remember that the parent of this commit is $PRIOR, not $RELEASE, despite
the review being sent to the latter in Gerrit.
EOF
  [ -z "$CHANGEID$TASK" ] || echo
  [ -z "$TASK" ] || echo "Task-number: $TASK"
  [ -z "$CHANGEID" ] || echo "Change-Id: $CHANGEID"
  ) > "$GITDIR/COMMIT_EDITMSG"
# The git status in that holds a lock that precludes the git commit;
# so we can't just pipe the message and use -F - to deliver it.
run git commit $QUIET $AMEND -F "$GITDIR/COMMIT_EDITMSG"

mention "I recommend you review that what git diff -D reports (now) *is* boring."
mention "(If any of it isn't, you can git add -p it and git commit --amend.)"
mention "Then you can: git push gerrit $BRANCH:refs/for/$RELEASE"
